{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 2.2 扩展答疑机器人的知识范围\n",
    "\n",
    "## 🚄 前言\n",
    "\n",
    "你已经了解到RAG是扩展大模型知识范围的有效解决方案。本节你将学习RAG的工作流程，以及如何创建一个RAG应用，使它能够根据公司的制度文件来进行回答。\n",
    "\n",
    "## 🍁 课程目标\n",
    "\n",
    "学完本节课程后，你将能够：\n",
    "\n",
    "* 了解RAG的工作流程\n",
    "* 创建一个RAG应用"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. RAG的工作原理\n",
    "\n",
    "你在考试的时候有可能会因为忘记某个概念或公式而失去分数，但考试如果是开卷形式，那么你只需要找到与考题最相关的知识点，并加上你的理解就可以进行回答了。\n",
    "\n",
    "对于大模型来说也是如此，在训练过程中由于没有见过某个知识点（比如你们公司的制度文件），因此直接向它提问相关问题会得到不准确的答案；如果在大模型生成内容时，像开卷考试一样将相关知识提供给它作为参考，那么大模型回答的质量也就会大幅提高了。\n",
    "\n",
    "这引出了之前提到的一个核心理念：**上下文工程（Context Engineering）**，专注于为大模型的“上下文窗口”填充恰到好处的信息，以引导其完成特定任务。如果信息太少，模型会“不知道”；如果信息太多或无关，模型的性能会下降，成本也会增加。\n",
    "\n",
    "而我们即将学习的 **RAG（Retrieval Augmented Generation，检索增强生成）**，正是上下文工程中最重要、最有效的技术之一，专门解决大模型“知识不足”的问题。RAG应用通常包含**建立索引**与**检索生成**两部分。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### 1.1 建立索引\n",
    "你可能会在考试前对参考资料做标记，来帮助你在考试时更容易地找到相关信息。类似的，RAG应用往往也会在回答前就已经做好了标记，这一过程叫做**建立索引**，建立索引包括四个步骤：<br>\n",
    "1. **文档解析**<br>\n",
    "就像你会将书上看到的视觉信息理解为文字信息一样，RAG应用也需要首先将知识库文档进行加载并解析为大模型能够理解的文字形式。\n",
    "2. **文本分段**<br>\n",
    "你通常不会在做某道题时把整本书都翻阅一遍，而是去查找与问题最相关的几个段落，因此你会先把参考资料做一个大致的分段。类似的，RAG应用也会在文档解析后对文本进行分段，以便于在后续能够快速找到与提问最相关的内容。\n",
    "3. **文本向量化**<br>\n",
    "在开卷考试时，你通常会先在参考资料中寻找与问题最相关的段落，再去进行作答。在RAG应用中，通常需要借助嵌入（embedding）模型分别对段落与问题进行数字化表示，在进行相似度比较后找出最相关的段落，数字化表示的过程就叫做文本向量化。<br>\n",
    "    > 如果你对这一过程的细节感兴趣，可以学习-本教程的拓展阅读部分。\n",
    "4. **存储索引**<br>\n",
    "存储索引将向量化后的段落存储为向量数据库，这样RAG应用就无需在每次进行回复时都重复以上步骤，从而可以增加响应速度。\n",
    "\n",
    "    <img src=\"https://gw.alicdn.com/imgextra/i2/O1CN010zLf411zVoZQ9cWsI_!!6000000006720-2-tps-1592-503.png\" width=\"600\"><br>\n",
    "\n",
    "    在建立索引后，RAG应用就可以根据用户的问题检索出相关的文本段了。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### 1.2 检索生成\n",
    "检索、生成分别对应着RAG名字中的`Retrieval`与`Generation`两阶段。**检索**就像开卷考试时去查找资料的过程，**生成**则是在找到资料后，根据参考资料与问题进行作答的过程。<br>\n",
    "1. **检索**<br>\n",
    "检索阶段会召回与问题最相关的文本段。通过embedding模型对问题进行文本向量化，并与向量数据库的段落进行语义相似度的比较，找出最相关的段落。检索是RAG应用中最重要的环节，你可以想象如果考试的时候找到了错误的资料，那么回答一定是不准确的。这个步骤完美诠释了上下文工程的精髓：从海量知识中“精准地选择相关信息”来填充上下文。找到最匹配的内容，是保证后续生成质量的第一步。为了提高检索准确性，除了使用性能强大的embedding模型，也可以做重排（rerank）、句子窗口检索等方法，这些内容你可以在之后的章节进行学习。\n",
    "2. **生成**<br>\n",
    "在检索到相关的文本段后，RAG应用会将问题与文本段通过提示词模板生成最终的提示词，由大模型生成回复，这个阶段更多是利用大模型的总结能力，而不是大模型本身具有的知识。这个提示词模板的设计，是上下文工程的另一个关键环节。我们不仅要提供检索到的“资料”，还要明确地“指导”模型如何使用这些资料来回答问题。\n",
    "    > 一个典型的提示词模板为：`请根据以下信息回答用户的问题：{召回文本段}。用户的问题是：{question}。`\n",
    "\n",
    "    <img src=\"https://img.alicdn.com/imgextra/i1/O1CN01vbkBXC1HQ0SBrC1Ii_!!6000000000751-2-tps-1776-639.png\" width=\"600\"><br>\n",
    "\n",
    "\n",
    "<!-- \n",
    "它的整体流程图为：\n",
    "<img src=\"https://img.alicdn.com/imgextra/i1/O1CN01GjUPUs1dIbaxdDowf_!!6000000003713-2-tps-1971-1221.png\" width=\"600\" />\n",
    "1. **文档解析**<br>\n",
    "就像你将书上看到的信息理解为文字信息一样，RAG应用也需要首先将知识库文档进行加载并解析为大模型能够理解的文字形式；\n",
    "2. **文本分段**<br>\n",
    "你通常不会在做某道题时把整本书都翻阅一遍，而是查找与问题最相关的几个段落，分段方法可能是按照目录中的标题等方法，RAG应用也会在解析后将文本分段，以便于后续的检索。\n",
    "3. **索引建立**<br>\n",
    "在开卷考试时，通常会先找到与问题最相关的几个段落，然后根据这些段落进行回答。RAG应用也会使用嵌入模型（embedding 模型）对切片后的文本段建立索引，并以向量数据库形式存储，便于后续检索。\n",
    "使用嵌入模型（embedding 模型）对切片后的文档建立索引，并以向量数据库形式存储，便于后续检索；\n",
    "4. 发起提问：用户输入的问题与向量数据库的知识进行匹配，并通过大模型生成结合知识库与用户问题的回复。 -->"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. 创建RAG应用\n",
    "\n",
    "构建一个RAG应用需要实现以上功能，这个过程并不容易。但通过LlamaIndex，你不需要过多代码就可以完成上述功能。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.1 请确认你当前的Python环境\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "> 在执行本节课程的代码前，请确认你已经切换到新创建的python环境，如前面课程创建的 `Python (llm_learn)`。\n",
    "> \n",
    "> <img src=\"https://img.alicdn.com/imgextra/i3/O1CN01rn0jvB1Z1QJXUWaG2_!!6000000003134-0-tps-3138-914.jpg\" width=\"600\">\n",
    ">\n",
    "> **注意：在接下来的每一节课程中，你都应该检查是否需要手动切换 Notebook 环境。**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.2 一个简单的RAG应用\n",
    "\n",
    "与上一节教程一样，你需要运行以下代码将百炼API Key配置到环境中。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T09:00:41.030766Z",
     "iopub.status.busy": "2024-11-15T09:00:41.030362Z",
     "iopub.status.idle": "2024-11-15T09:00:41.236899Z",
     "shell.execute_reply": "2024-11-15T09:00:41.236115Z",
     "shell.execute_reply.started": "2024-11-15T09:00:41.030739Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "from config.load_key import load_key\n",
    "import os\n",
    "\n",
    "load_key()\n",
    "# 生产环境中请勿将 API Key 输出到日志中，避免泄露\n",
    "print(f'''你配置的 API Key 是：{os.environ[\"DASHSCOPE_API_KEY\"][:5]+\"*\"*5}''')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们在docs文件夹中准备了一些虚构的公司制度文件，接下来你将基于这些文件来创建RAG应用。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T09:00:43.822829Z",
     "iopub.status.busy": "2024-11-15T09:00:43.822278Z",
     "iopub.status.idle": "2024-11-15T09:00:58.744414Z",
     "shell.execute_reply": "2024-11-15T09:00:58.743812Z",
     "shell.execute_reply.started": "2024-11-15T09:00:43.822800Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# 导入依赖\n",
    "from llama_index.embeddings.dashscope import DashScopeEmbedding,DashScopeTextEmbeddingModels\n",
    "from llama_index.core import SimpleDirectoryReader,VectorStoreIndex\n",
    "from llama_index.llms.openai_like import OpenAILike\n",
    "\n",
    "# 这两行代码是用于消除 WARNING 警告信息，避免干扰阅读学习，生产环境中建议根据需要来设置日志级别\n",
    "import logging\n",
    "logging.basicConfig(level=logging.ERROR)\n",
    "\n",
    "print(\"正在解析文件...\")\n",
    "# LlamaIndex提供了SimpleDirectoryReader方法，可以直接将指定文件夹中的文件加载为document对象，对应着解析过程\n",
    "documents = SimpleDirectoryReader('./docs').load_data()\n",
    "\n",
    "print(\"正在创建索引...\")\n",
    "# from_documents方法包含切片与建立索引步骤\n",
    "index = VectorStoreIndex.from_documents(\n",
    "    documents,\n",
    "    # 指定embedding 模型\n",
    "    embed_model=DashScopeEmbedding(\n",
    "        # 你也可以使用阿里云提供的其它embedding模型：https://help.aliyun.com/zh/model-studio/getting-started/models#3383780daf8hw\n",
    "        model_name=DashScopeTextEmbeddingModels.TEXT_EMBEDDING_V2\n",
    "    ))\n",
    "print(\"正在创建提问引擎...\")\n",
    "query_engine = index.as_query_engine(\n",
    "    # 设置为流式输出\n",
    "    streaming=True,\n",
    "    # 此处使用qwen-plus模型，你也可以使用阿里云提供的其它qwen的文本生成模型：https://help.aliyun.com/zh/model-studio/getting-started/models#9f8890ce29g5u\n",
    "    llm=OpenAILike(\n",
    "        model=\"qwen-plus\",\n",
    "        api_base=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n",
    "        api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n",
    "        is_chat_model=True\n",
    "        ))\n",
    "print(\"正在生成回复...\")\n",
    "streaming_response = query_engine.query('我们公司项目管理应该用什么工具')\n",
    "print(\"回答是：\")\n",
    "# 采用流式输出\n",
    "streaming_response.print_response_stream()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.3 保存与加载索引\n",
    "你可能会发现，创建索引消耗的时间比较长。如果能够将索引保存到本地，并在需要使用的时候直接加载，而不是重新建立索引，那就可以大幅提升回复的速度，LlamaIndex提供了简单易实现的保存与加载索引的方法。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T09:00:59.966266Z",
     "iopub.status.busy": "2024-11-15T09:00:59.965889Z",
     "iopub.status.idle": "2024-11-15T09:01:00.240477Z",
     "shell.execute_reply": "2024-11-15T09:01:00.239682Z",
     "shell.execute_reply.started": "2024-11-15T09:00:59.966241Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# 将索引保存为本地文件\n",
    "index.storage_context.persist(\"knowledge_base/test\")\n",
    "print(\"索引文件保存到了knowledge_base/test\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T09:01:02.142167Z",
     "iopub.status.busy": "2024-11-15T09:01:02.141798Z",
     "iopub.status.idle": "2024-11-15T09:01:02.675970Z",
     "shell.execute_reply": "2024-11-15T09:01:02.675221Z",
     "shell.execute_reply.started": "2024-11-15T09:01:02.142142Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# 将本地索引文件加载为索引\n",
    "from llama_index.core import StorageContext,load_index_from_storage\n",
    "storage_context = StorageContext.from_defaults(persist_dir=\"knowledge_base/test\")\n",
    "index = load_index_from_storage(storage_context,embed_model=DashScopeEmbedding(\n",
    "        model_name=DashScopeTextEmbeddingModels.TEXT_EMBEDDING_V2\n",
    "    ))\n",
    "print(\"成功从knowledge_base/test路径加载索引\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "从本地加载索引后，你可以再次进行提问测试是否可以正常工作。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T09:01:11.982327Z",
     "iopub.status.busy": "2024-11-15T09:01:11.981943Z",
     "iopub.status.idle": "2024-11-15T09:01:14.921721Z",
     "shell.execute_reply": "2024-11-15T09:01:14.921129Z",
     "shell.execute_reply.started": "2024-11-15T09:01:11.982304Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "print(\"正在创建提问引擎...\")\n",
    "query_engine = index.as_query_engine(\n",
    "    # 设置为流式输出\n",
    "    streaming=True,\n",
    "    # 此处使用qwen-plus模型，你也可以使用阿里云提供的其它qwen的文本生成模型：https://help.aliyun.com/zh/model-studio/getting-started/models#9f8890ce29g5u\n",
    "    llm=OpenAILike(\n",
    "        model=\"qwen-plus\",\n",
    "        api_base=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n",
    "        api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n",
    "        is_chat_model=True\n",
    "        ))\n",
    "print(\"正在生成回复...\")\n",
    "streaming_response = query_engine.query('我们公司项目管理应该用什么工具')\n",
    "print(\"回答是：\")\n",
    "streaming_response.print_response_stream()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "你可以将上述代码进行封装，以便在后续持续迭代时快速使用。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T09:01:16.991663Z",
     "iopub.status.busy": "2024-11-15T09:01:16.991276Z",
     "iopub.status.idle": "2024-11-15T09:01:20.492123Z",
     "shell.execute_reply": "2024-11-15T09:01:20.491499Z",
     "shell.execute_reply.started": "2024-11-15T09:01:16.991640Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "from chatbot import rag\n",
    "\n",
    "# 引文在前面的步骤中已经建立了索引，因此这里可以直接加载索引。如果需要重建索引，可以增加一行代码：rag.indexing()\n",
    "index = rag.load_index(persist_path='./knowledge_base/test')\n",
    "query_engine = rag.create_query_engine(index=index)\n",
    "\n",
    "rag.ask('我们公司项目管理应该用什么工具', query_engine=query_engine)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 📝3.本节小结\n",
    "在本节课程中，你不仅动手构建了一个RAG应用，更重要的是，你进一步深化了对 **上下文工程（Context Engineering）** 这一核心理念的理解与实践。\n",
    "\n",
    "我们来回顾一下：\n",
    "\n",
    "1. **理解了RAG是上下文工程的关键实践**<br>\n",
    "你学习到，上下文工程是为大模型准备“恰到好处”信息的艺术。而RAG正是实现这一目标的核心技术，它通过“建立索引”和“检索生成”两个阶段，为大模型动态地提供了外部知识，解决了它不知道最新或私有信息的问题。\n",
    "\n",
    "2. **实践了包含上下文管理的RAG应用** <br>\n",
    "通过LlamaIndex，你快速地创建了一个RAG应用，并通过构建、保存和加载索引，高效地管理了外部知识库。\n",
    "\n",
    "你的答疑机器人已经可以基于公司文件回答问题，并能处理简单的多轮对话了。这再次证明了上下文工程的巨大威力：我们没有重新训练模型，仅仅通过巧妙地构建和管理上下文，就显著扩展了它的能力边界。在后续课程中，你将学习更多高级的上下文工程技术，进一步提升机器人的性能。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 拓展阅读\n",
    "\n",
    "#### 文本向量化\n",
    "计算机并不能直接理解“我喜欢吃苹果”与“我爱吃苹果”这两句话到底有多相似，但它能理解两个相同维度的向量的相似度（通常使用余弦相似度来衡量）。文本向量化通过embedding模型，将自然语言转化为计算机能够理解的数字形式。\n",
    "\n",
    "embedding模型的训练通常会包含**对比学习**的环节，输入数据是许多已标记为是否相关的文本对(s1,s2)，模型的训练目标是尽可能让相关的文本对生成的向量相似度变高，不相关的文本对生成的向量相似度变低。\n",
    "\n",
    "在**建立索引**阶段，假设已经通过文本分段获得了n个chunk：[c1,c2,c3,...,cn]，那么embedding模型会将这n个chunk分别转化为向量：[v1,v2,v3,...,vn]，并存储为向量数据库。\n",
    "\n",
    "在**检索**阶段，假设用户的问题为q，那么embedding模型会将问题q转化为向量vq，并在向量数据库中找出与vq最相似的n个向量（这个值你可以自己设定），通过向量与文本段的索引关系得到文本段，作为检索结果。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 🔥 课后小测验\n",
    "### 🔍 单选题\n",
    "\n",
    "<details>\n",
    "<summary style=\"cursor: pointer; padding: 12px; border: 1px solid #dee2e6; border-radius: 6px;\">\n",
    "<b>在RAG应用中进行多轮对话，应该如何进行检索❓</b>\n",
    "\n",
    "- A. 在检索阶段输入完整的历史对话信息<br>\n",
    "- B. 结合历史对话信息对输入问题改写后进入检索阶段<br>\n",
    "- C. 在检索阶段输入最新的输入问题<br>\n",
    "- D. 将上一轮召回的文本段迁移过来<br>\n",
    "\n",
    "**【点击查看答案】**\n",
    "</summary>\n",
    "\n",
    "<div style=\"margin-top: 10px; padding: 15px;  border: 1px solid #dee2e6; border-radius: 0 0 6px 6px;\">\n",
    "\n",
    "✅ **参考答案：B**  \n",
    "📝 **解析**：  \n",
    "- 在多轮对话中，直接使用原始问题（如选项 C ）或完整历史记录（选项 A ）会导致检索噪声或信息冗余。\n",
    "- 而选项 B 通过动态重写当前问题，既保留了对话连贯性，又避免了选项 D 的过时文本迁移问题，是平衡效率与精度的最优方案。\n",
    "\n",
    "</div>\n",
    "</details>\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ✅评价反馈\n",
    "欢迎你参与[阿里云大模型ACP课程问卷](https://survey.aliyun.com/apps/zhiliao/Mo5O9vuie) 反馈学习体验和课程评价。\n",
    "你的批评和鼓励都是我们前进的动力！"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "learnacp",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
